1 /**
2 The keyedcollection module contains:
3   $(TOC Enforce)
4   $(TOC usableForKeyedCollection)
5   $(TOC BaseKeyedCollection)
6   $(TOC KeyedCollection)
7 
8 License: $(GPL2)
9 
10 Authors: Matthew Armbruster
11 
12 $(B Source:) $(SRC $(SRCFILENAME))
13 
14 Copyright: 2016
15  */
16 module db_constraints.keyed.keyedcollection;
17 
18 import std.algorithm : canFind, endsWith, each;
19 import std.conv : to;
20 import std.exception : enforceEx;
21 import std.traits;
22 import std.typecons : Flag, Yes, No;
23 
24 import db_constraints.db_exceptions;
25 import db_constraints.keyed.keyeditem;
26 import db_constraints.utils.meta;
27 
28 /**
29 Tells the keyed collection which constraints to check.
30  */
31 enum Enforce
32 {
33 /**
34 Set $(SRCTAG KeyedCollection.enforceConstraints) equal to this if you do
35 not want any constraints to be enforced.
36  */
37     none = 0,
38 /**
39 Enforce the item's check constraint meaning anything with
40 $(WIKI constraints, NotNull) or $(WIKI constraints, CheckConstraint).
41 
42 Not using this means an item will not be checked when it is added
43 to the collection. If you set up the singular class like the examples
44 though the setter method will still check constraints.
45  */
46     check = 1 << 0,
47 /**
48 Enforce the collection does not already contain
49 the item you are trying to add. Makes sure there would not
50 be conflicting clustered indicies.
51  */
52     clusteredUnique = 1 << 1,
53 /**
54 Enforce all unique constraints are not being violated. If
55 you have this then you do not need to have clusteredUnique.
56  */
57     unique = 1 << 2,
58 /**
59 Enforce the foreign key constraints if there are any.
60  */
61     foreignKey = 1 << 3,
62 /**
63 Enforce the exclusion constraints if there are any.
64 
65 Version: \>= 0.0.7
66  */
67     exclusion = 1 << 4
68 }
69 
70 /**
71 Makes sure the class is usable for keyed collection. This really just
72 makes sure it has the necessary members that come with keyeditem.
73 Returns:
74     true if class can be used for keyed collection
75  */
76 template usableForKeyedCollection(alias T)
77 {
78     enum usableForKeyedCollection = ( is(T == class) &&
79         __traits(compiles,
80                  (T t, T i)
81                  {
82                      if (i.key == t.key) { }
83                      class Example
84                      {
85                          void itemChanged(string s, typeof(T.key) k) { }
86                          void add(T item)
87                          {
88                              item.emitChange.connect(&itemChanged);
89                          }
90                      }
91                      t.checkConstraints();
92                      t.markAsSaved();
93                      auto j = new Example();
94                      j.add(t);
95                      string k = t.toString;
96                  }));
97 }
98 
99 /**
100 Turns the inheriting class into a base keyed collection.
101 The key is based on the singular class' clustered index.
102 The requirements are taken care of when
103 you include the keyeditem in the $(I T) class.
104 If you plan on changing the singular class' clustered index,
105 you must define $(D dup()) that returns a new instance of your class.
106 
107 If $(D T) has foreign keys you must use $(SRCTAG KeyedCollection) instead
108 since the functions that come with foreign keys need to have the
109 other class imported.
110 
111 This also allows you to make a keyed collection in one line.
112 $(D_CODE alias Candies = BaseKeyedCollection!(Candy);)
113 Now you can use Candies as a collection.
114 Params:
115     T = the singular class
116  */
117 class BaseKeyedCollection(T)
118     if (usableForKeyedCollection!(T))
119 {
120     mixin KeyedCollection!(T);
121 }
122 
123 
124 /**
125 Turns the inheriting class into a keyed collection.
126 The key is based on the singular class' clustered index.
127 The requirements (except for dup) are taken care of when
128 you include the keyeditem in the $(I T) class.
129 
130 $(D T) should represent a single row in the database. Use
131 this when $(D T) has foreign keys.
132  */
133 mixin template KeyedCollection(T)
134     if (usableForKeyedCollection!(T))
135 {
136     import std.algorithm : canFind, endsWith, each, filter;
137     import std.signals;
138     import std.traits : isIterable;
139 
140 
141 /**
142 The $(D key_type) is alias'd as the type since it looked better than having
143 $(D typeof(T.key)) everywhere.
144  */
145     final alias key_type = typeof(T.key);
146 /**
147 Alias letting you know what this is a collection of.
148 
149 Version: \>= 0.0.6
150  */
151     final alias collectionof = T;
152 
153     private bool _containsChanges;
154     private ubyte _enforceConstraints = (Enforce.check |
155                                          Enforce.unique |
156                                          Enforce.foreignKey |
157                                          Enforce.exclusion);
158 
159     static if (hasForeignKeys!(T))
160     {
161         mixin(createForeignKeyProperties!(T));
162 /**
163 Called when you associate a foreign key or an item changed. This checks
164 the current items against its foreign keyed class.
165  */
166         final private void checkForeignKeys()
167         {
168             this.byValue.each!(
169                 (T a) =>
170                 {
171                     mixin(createForeignKeyCheckExceptions!(T));
172                 }());
173         }
174         /// ditto
175         final private void checkForeignKeys(T a)
176         {
177             mixin(createForeignKeyCheckExceptions!(T));
178         }
179         mixin(createForeignKeyChanged!(T));
180     }
181 /**
182 Called when an item is being added or an item changed. This checks
183 the item's check constraints, unique constraints, and foreign key constraints.
184  */
185     final private void checkConstraints(T item)
186     {
187         if (_enforceConstraints & Enforce.check)
188         {
189             item.checkConstraints();
190         }
191         if (_enforceConstraints & Enforce.clusteredUnique)
192         {
193             auto i = (item in this);
194             enforceEx!UniqueConstraintException(
195                 (i is null || (*i) is item),
196                 "The " ~ key_type.stringof ~ " constraint for class " ~
197                 T.stringof ~
198                 "  was violated by item " ~ item.toString ~ ".");
199         }
200         if (_enforceConstraints & Enforce.unique)
201         {
202             auto constraintName = "";
203             enforceEx!UniqueConstraintException(
204                 !violatesUniqueConstraints(item, constraintName),
205                 "The " ~ constraintName ~ " constraint for class " ~
206                 T.stringof ~
207                 " was violated by item " ~ item.toString ~ ".");
208         }
209         if (_enforceConstraints & Enforce.foreignKey)
210         {
211             static if (hasForeignKeys!(T))
212             {
213                 checkForeignKeys(item);
214             }
215         }
216         if (_enforceConstraints & Enforce.exclusion)
217         {
218             auto constraintName = "";
219             enforceEx!ExclusionConstraintException(
220                 !violatesExclusionConstraints(item, constraintName),
221                 "The " ~ constraintName ~ " constraint for class " ~
222                 T.stringof ~
223                 " was violated by item " ~ item.toString ~ ".");
224         }
225     }
226     /// ditto
227     final private void checkConstraints(key_type item_key)
228     {
229         checkConstraints(this[item_key]);
230     }
231 /**
232 $(D itemChanged) is connected to the signal emitted by the item. This checks
233 constraints and makes sure the changes are acceptable.
234 
235 $(THROWS KeyedException, if $(D dup()) is not defined and you change the
236 clustered index.)
237  */
238     final private void itemChanged(string propertyName, key_type item_key)
239     {
240         key_type emit_key = item_key;
241         if (propertyName == "key")
242         {
243             static if ( __traits(compiles,
244                                  (T t)
245                                  {
246                                      T i = t.dup();
247                                  }))
248             {
249                 T item = this._items[item_key].dup();
250                 this.remove(item_key, No.notifyChange);
251                 this.add(item, No.notifyChange);
252                 emit_key = item.key;
253             }
254             else
255             {
256                 enum msg = T.stringof ~ " is trying to change its clustered " ~
257                     "index without defining a dup() function.";
258                 throw new KeyedException(msg);
259             }
260         }
261         else if (propertyName.endsWith("_key"))
262         {
263             checkConstraints(item_key);
264         }
265         notify(propertyName, emit_key);
266     }
267     T[key_type] _items;
268 /**
269 The signal used to emit changes that occur in $(D this).
270  */
271     mixin Signal!(string, key_type) collectionChanged;
272 /**
273 Changes $(D this) to not contain changes and also marks all
274 the items as saved. Should only be used after a save.
275  */
276     final void markAsSaved() nothrow pure @nogc
277     {
278         _containsChanges = false;
279         this.byValue.each!(a => a.markAsSaved());
280     }
281 /**
282 Read-only property telling if $(D this) contains changes.
283 Returns:
284     true if $(D this) contains changes.
285  */
286     final @property bool containsChanges() const nothrow pure @safe @nogc
287     {
288         return _containsChanges;
289     }
290 /**
291 Write-only property to enforce the constraints. By default
292 this is  $(D (Enforce.check | Enforce.unique | Enforce.foreignKey | Enforce.exclusion))
293 but you may set it to 0 if you have a lot of
294 initial data and already trust that it does not violate any constraints.
295 
296 Setting this to false means that there are no checks and if there
297 is a duplicate clustered index, it will be overwritten.
298 */
299     final @property void enforceConstraints(ubyte value) nothrow pure @safe @nogc
300     {
301         _enforceConstraints = value;
302     }
303 
304 /**
305 Notifies $(D this) which property changed and sets containsChanges to true.
306 This also emits a signal with the property name that changed
307 and the key to it in this collection.
308 Params:
309     propertyName = the property name that changed
310     item_key = the items key that changed
311  */
312     final void notify()(string propertyName, key_type item_key)
313     {
314         _containsChanges = true;
315         collectionChanged.emit(propertyName, item_key);
316     }
317 /**
318 Removes an item from $(D this) and disconnects the signals. Notifies
319 that the length of $(D this) has changed by emitting "remove".
320  */
321     final void remove(key_type item_key, Flag!"notifyChange" notifyChange = Yes.notifyChange)
322     {
323         if (this.contains(item_key))
324         {
325             this._items[item_key].disconnect(&itemChanged);
326             this._items.remove(item_key);
327             if (notifyChange)
328             {
329                 notify("remove", item_key);
330             }
331         }
332     }
333     /// ditto
334     final void remove(T item)
335     in
336     {
337         assert(item !is null, "Trying to remove a null item.");
338     }
339     body
340     {
341         this.remove(item.key);
342     }
343     /// ditto
344     final void remove(A...)(A a)
345     in
346     {
347         static assert(A.length == key_type.tupleof.length, T.stringof ~
348                       " has a clustered index with " ~
349                       key_type.tupleof.length.to!string ~
350                       " member(s). You included " ~ A.length.to!string ~
351                       " members when using remove.");
352     }
353     body
354     {
355         auto clIdx = key_type(a);
356         return this.remove(clIdx);
357     }
358 /**
359 Adds $(D item) to $(D this) and connects to the signals emitted by $(D item).
360 Notifies that the length of $(D this) has changed.
361 
362 $(THROWS UniqueConstraintException, if $(D this) already contains $(D item) and
363 enforceConstraints include $(SRCTAG Enforce.unique) or
364 $(SRCTAG Enforce.clusteredUnique).)
365 
366 $(THROWS CheckConstraintException, if the item is violating any of its
367 defined check constraints and enforceConstraints include
368 $(SRCTAG Enforce.check).)
369 
370 $(THROWS ForeignKeyException, if the item is violating any of its
371 foreign key constraints and enforceConstraints include
372 $(SRCTAG Enforce.foreignKey).)
373 
374 $(THROWS ExclusionConstraintException, if $(D item) conflicts with any item
375 in $(D this) via the ExclusionConstraint and enforceConstraint includes
376 $(SRCTAG Enforce.exclusion).)
377 
378 $(B Precondition:) $(D_CODE assert(item(s) !is null);)
379 
380 Params:
381     item(s) = the item(s) you want to add to $(D this)
382     notifyChange = whether or not to emit this change. Should only be No if coming from itemChanged
383  */
384     final void add(T item, Flag!"notifyChange" notifyChange = Yes.notifyChange)
385     in
386     {
387         assert(item !is null, "Trying to add a null item.");
388     }
389     body
390     {
391         this.checkConstraints(item);
392         item.emitChange.connect(&itemChanged);
393         this._items[item.key] = item;
394         if (notifyChange)
395         {
396             notify("add", item.key);
397         }
398     }
399     /// ditto
400     final void add(I)(I items, Flag!"notifyChange" notifyChange = Yes.notifyChange)
401         if (isIterable!(I))
402     in
403     {
404         assert(items !is null, "Trying to add a null array");
405     }
406     body
407     {
408         foreach(item; items)
409         {
410             assert(is(typeof(item) == T));
411             this.add(item, notifyChange);
412         }
413     }
414 /**
415 This just calls $(SRCTAG KeyedCollection.add).
416  */
417     final ref auto opOpAssign(string op : "~")(T item)
418     {
419         this.add(item);
420     }
421     /// ditto
422     final ref auto opOpAssign(string op : "~", I)(I items)
423         if (isIterable!(I))
424     {
425         this.add(items);
426     }
427 
428 /**
429 Initializes $(D this). Adds $(D item) to $(D this) and connects to the signals
430 emitted by $(D item).
431 
432 This just calls $(SRCTAG KeyedCollection.add).
433 
434 $(B Precondition:) $(D_CODE assert(item(s) !is null);)
435  */
436     final this(T item)
437     in
438     {
439         assert(item !is null, "Trying to initialize with a null " ~ T.stringof ~ ".");
440     }
441     body
442     {
443         this.add(item, No.notifyChange);
444     }
445     /// ditto
446     final this(I)(I items)
447         if (isIterable!(I))
448     in
449     {
450         assert(items !is null, "Trying to initialize with a null iterable.");
451     }
452     body
453     {
454         this.add(items, No.notifyChange);
455     }
456     /// ditto
457     final this()
458     {
459     }
460 
461 
462 /**
463 Gets the approriate $(D T). You can either use an item
464 that equals the item you want back, a key of the item you want
465 back or parameters that can make the key for the item you want back.
466 Returns:
467     The item in the collection that matches $(D item).
468 
469 $(THROWS KeyedException, if $(D this) does not contain a matching
470 clustered index.)
471 
472 $(B Precondition:) $(D_CODE assert(item !is null);)
473  */
474     final ref inout(T) opIndex(in T item) inout
475     in
476     {
477         assert(item !is null, "Trying to lookup with a null.");
478     }
479     body
480     {
481         return this[item.key];
482     }
483     /// ditto
484     final ref inout(T) opIndex(in key_type clIdx) inout
485     {
486         if (this.contains(clIdx))
487         {
488             return this._items[clIdx];
489         }
490         else
491         {
492             auto fields = "\nAn item with clustered index of:\n";
493             foreach(i, j; clIdx.tupleof)
494             {
495                 fields ~= clIdx.tupleof[i].stringof ~ " = " ~ j.to!string() ~ "\n";
496             }
497             fields ~= "does not exist in " ~ typeof(this).stringof;
498             throw new KeyedException(fields);
499         }
500     }
501     /// ditto
502     final ref inout(T) opIndex(A...)(in A a) inout
503     in
504     {
505         static assert(A.length == key_type.tupleof.length, T.stringof ~
506                       " has a clustered index with " ~
507                       key_type.tupleof.length.to!string ~
508                       " member(s). You included " ~ A.length.to!string ~
509                       " members when using the index.");
510     }
511     body
512     {
513         auto clIdx = key_type(a);
514         return this[clIdx];
515     }
516 /**
517 Forwards all methods not specified by this abstract class
518 to the private associative array.
519  */
520     auto opDispatch(string name, A...)(A a)
521     {
522         debug(dispatch) pragma(msg, "opDispatch", name);
523         return mixin("this._items." ~ name ~ "(a)");
524     }
525 /**
526 Allows you to use $(D this) in a foreach loop.
527  */
528     final int opApply(int delegate(ref T) dg)
529     {
530         int result = 0;
531         foreach(T i; this.values)
532         {
533             result = dg(i);
534             if (result)
535                 break;
536         }
537         return result;
538     }
539     /// ditto
540     final int opApply(int delegate(key_type, ref T) dg)
541     {
542         int result = 0;
543         foreach(T i; this.values)
544         {
545             result = dg(i.key, i);
546             if (result)
547                 break;
548         }
549         return result;
550     }
551 /**
552 Gets the length of the collection.
553 Returns:
554     The number of items in the collection.
555  */
556     final size_t length() const @property @safe nothrow pure
557     {
558         return this._items.length;
559     }
560 /**
561 Checks if $(D item) is in the collection.
562 Params:
563     item = the item you want to see is in the collection
564 Returns:
565     true if $(D item) is in the collection.
566  */
567     final bool contains(in T item) const nothrow pure @safe @nogc
568     {
569         return this.contains(item.key);
570     }
571     /// ditto
572     final bool contains(in key_type clIdx) const nothrow pure @safe @nogc
573     {
574         auto i = (clIdx in this._items);
575         return (i !is null);
576     }
577     /// ditto
578     final bool contains(A...)(in A a) const nothrow pure @safe @nogc
579     in
580     {
581         static assert(A.length == key_type.tupleof.length, T.stringof ~
582                       " has a clustered index with " ~
583                       key_type.tupleof.length.to!string ~
584                       " member(s). You included " ~ A.length.to!string ~
585                       " members when using contains.");
586     }
587     body
588     {
589         auto clIdx = key_type(a);
590         return this.contains(clIdx);
591     }
592 /**
593 The $(WEB dlang.org/expression.html#InExpression, InExpression) yields a pointer
594 to the value if the key is in the associative array, or null if not.
595  */
596     final inout(T)* opBinaryRight(string op : "in")(in T item) inout nothrow pure @safe @nogc
597     {
598         return (item.key in this);
599     }
600     /// ditto
601     final inout(T)* opBinaryRight(string op : "in")(in key_type clIdx) inout nothrow pure @safe @nogc
602     {
603         return (clIdx in this._items);
604     }
605     /// ditto
606     final inout(T)* opBinaryRight(string op : "in", A...)(in A a) inout nothrow pure @safe @nogc
607     in
608     {
609         static assert(A.length == key_type.tupleof.length, T.stringof ~
610                       " has a clustered index with " ~
611                       key_type.tupleof.length.to!string ~
612                       " member(s). You included " ~ A.length.to!string ~
613                       " members when using 'in'.");
614     }
615     body
616     {
617         auto clIdx = key_type(a);
618         return (clIdx in this);
619     }
620 /**
621 Checks if the item has any conflicting unique constraints. This
622 is more extensive than $(SRCTAG KeyedCollection.contains).
623 
624 $(B Precondition:) $(D_CODE assert(items !is null);)
625 
626 $(B Postcondition:)
627 $(D_CODE
628 if (result)
629     assert(constraintName !is null && constraintName != "");
630 else
631     assert(constraintName is null);
632 )
633  */
634     final bool violatesUniqueConstraints(in T item, out string constraintName) const nothrow pure
635     in
636     {
637         assert(item !is null, "Cannot check if a null item is duplicated.");
638     }
639     out (result)
640     {
641         if (result)
642             assert(constraintName !is null && constraintName != "");
643         else
644             assert(constraintName is null);
645     }
646     body
647     {
648         bool result = false;
649         foreach(uniqueName; GetUniqueConstraintStructNames!(T))
650         {
651             if (this._items.byValue.canFind!("a !is b && " ~
652                                       "a." ~ uniqueName ~ "_key == " ~
653                                       "b." ~ uniqueName ~ "_key")(item))
654             {
655                 result = true;
656                 if (constraintName is null)
657                 {
658                     constraintName = uniqueName;
659                 }
660                 else
661                 {
662                     constraintName ~= ", " ~ uniqueName;
663                 }
664             }
665         }
666         return result;
667     }
668     /// ditto
669     final bool violatesUniqueConstraints(in T item) const nothrow pure
670     {
671         string constraintName;
672         return this.violatesUniqueConstraints(item, constraintName);
673     }
674 
675 /**
676 Checks if the item has any conflicting exclusion constraints.
677 
678 $(B Precondition:) $(D_CODE assert(items !is null);)
679 
680 $(B Postcondition:)
681 $(D_CODE
682 if (result)
683     assert(constraintName !is null && constraintName != "");
684 else
685     assert(constraintName is null);
686 )
687  */
688     final bool violatesExclusionConstraints(in T item, out string constraintName)
689     in
690     {
691         assert(item !is null, "Cannot check if a null item is duplicated.");
692     }
693     out (result)
694     {
695         if (result)
696             assert(constraintName !is null && constraintName != "");
697         else
698             assert(constraintName is null);
699     }
700     body
701     {
702         bool result = false;
703         static if (hasExclusionConstraints!T)
704         {
705             foreach(exc; GetExclusionConstraints!T)
706             {
707                 foreach(i; this._items.byValue)
708                 {
709                     if (exc.exclusion(i, item))
710                     {
711                         result = true;
712                         if (constraintName is null)
713                         {
714                             constraintName = exc.name;
715                         }
716                         else
717                         {
718                             constraintName ~= ", " ~ exc.name;
719                         }
720                         break;
721                     }
722                 }
723             }
724         }
725 
726         return result;
727     }
728     /// ditto
729     final bool violatesExclusionConstraints(in T item)
730     {
731         string constraintName;
732         return this.violatesExclusionConstraints(item, constraintName);
733     }
734 }
735 
736 ///
737 unittest
738 {
739     // singular class this holds all of the columns
740     class Candy
741     {
742     private:
743         string _name;
744         int _ranking;
745     public:
746         // marking name as part of the primary key
747         @PrimaryKeyColumn @NotNull
748         @property string name() const nothrow pure @safe @nogc
749         {
750             return _name;
751         }
752         @property void name(string value)
753         {
754             setter(_name, value);
755         }
756         @property int ranking() const nothrow pure @safe @nogc
757         {
758             return _ranking;
759         }
760         // making sure that ranking will always be above 0
761         @CheckConstraint!(a => a > 0, "chk_Candys_ranking")
762         @property void ranking(int value)
763         {
764             setter(_ranking, value);
765         }
766 
767         this(string name, int ranking)
768         {
769             this._name = name;
770             this._ranking = ranking;
771             // need to initialize the keyed item
772             initializeKeyedItem();
773         }
774         Candy dup() const
775         {
776             return new Candy(this._name, this._ranking);
777         }
778         // the default is to make the primary key into the clustered index
779         // which allows you to search based on the primary key
780         mixin KeyedItem!();
781     }
782 
783     // plural class
784     // I am using an alias since BaseKeyedCollection
785     // takes care of everything I want to do for this example in one line.
786     alias Candies = BaseKeyedCollection!(Candy);
787 
788     // Candies is a collection of Candy
789     static assert(is(Candies.collectionof == Candy));
790 
791     // source:
792     // http://www.bloomberg.com/ss/09/10/1021_americas_25_top_selling_candies/
793     // should be Milky not Milkey, this is wrong on purpose
794     auto milkyWay = new Candy("Milkey Way", 18);
795     auto snickers = new Candy("Snickers", 4);
796     auto reesesPBCups = new Candy("Reese's Peanut Butter Cups", 2);
797 
798     auto mars = new Candies([milkyWay, snickers]);
799     assert(mars.length == 2);
800     assert(!mars.containsChanges);
801 
802     auto hershey = new Candies(reesesPBCups);
803     assert(hershey.length == 1);
804 
805     // use the class as an index and confirm it returns the correct value
806     assert(mars[milkyWay] is milkyWay);
807     // use the primary key as an index
808     auto pk = Candy.PrimaryKey("Milkey Way");
809     assert(pk.name == milkyWay.name);
810     assert(mars[pk] is milkyWay);
811     // use the contents of the primary key as an index
812     assert(mars["Milkey Way"] is milkyWay);
813 
814     // milky way is in mars
815     assert(mars.contains(pk));
816     // reesesPBCups is not in mars
817     assert(!mars.contains(reesesPBCups));
818 
819     // now we change the name to be correct
820     mars[pk].name = "Milky Way"; // remember pk is primary key for milky way
821 
822     // since we changed milky way's name, mars contains changes
823     assert(mars.containsChanges);
824 
825     // since we had name in pk spelled incorrectly
826     // and changed it, the primary key in mars has
827     // updated so Milkey Way is no longer in it but
828     // Milky Way is.
829     assert(!mars.contains("Milkey Way"));
830     assert(mars.contains("Milky Way"));
831 
832     // looping over mars we make sure the key can be used to get
833     // the correct value.
834     foreach(name_pk, candy; mars)
835     {
836         assert(mars[name_pk] == candy);
837     }
838 
839     // trying to add another candy with the same name will
840     // result in a unique constraint violation even if the ranking is different
841     auto milkyWay2 = new Candy("Milky Way", 16);
842     assert(milkyWay.name == milkyWay2.name);
843     assert(milkyWay.ranking != milkyWay2.ranking);
844     import std.exception : assertThrown;
845     assertThrown!(UniqueConstraintException)(mars ~= milkyWay2);
846 
847     // ranking has a check constraint saying ranking always must be greater
848     // than 0. setting it to -1 resolves in a CheckConstraintException.
849     assertThrown!(CheckConstraintException)(mars["Milky Way"].ranking = -1);
850     // Since name is part of the primary key we must mark it with NotNull
851     // trying to set this to null will result in a CheckConstraintException.
852     assertThrown!(CheckConstraintException)(mars["Milky Way"].name = null);
853 
854     // violatesUniqueConstraints will tell you which unique constraint
855     // is violated if any
856     string violatedConstraint;
857     assert(mars.violatesUniqueConstraints(milkyWay2, violatedConstraint));
858     assert(violatedConstraint !is null && violatedConstraint == "PrimaryKey");
859 
860     // removing milky way from mars
861     mars.remove(milkyWay);
862     // this means milkyWay2 is no longer a duplicate
863     assert(!mars.violatesUniqueConstraints(milkyWay2, violatedConstraint));
864     assert(violatedConstraint is null);
865 }